8. Funktioner

 

 

·     Vad är en funktion?

 

·     Retur: instruktion, typ och värde.

 

·     Anropsparametrar.

 

·     Anrop med pekare.

 

·     Prototyper.

 

·     Inlinefunktioner.

 

·     Macros.

 

·     Rekursion.

 

·     Förvalda argument.

 

·     Överlagrade funktioner.

 

·     Räckvidd.

 

·     Räckviddsoperatorn.

 

 

 


Definition av funktion.

 

·     En funktion är ett isolerat program, avsett att anropas av andra funktioner.

 

·     En funktion har ett namn, vilket används vid anrop.

 

·     En funktion kan ha en eller flera inparametrar, argument.

 

·     En funktion kan svara med ett returvärde av bestämd typ.

 

·     En funktion anses vara av en viss typ - samma typ som returvärdets typ.

 

·     En funktion som inte returnerar ett svar är av typen void (utan värde).

 

Ovanstående gäller oavsett var funktionen står. Den kan stå som källkod i början av ditt program, den kan stå som källkod i en annan .c-fil som tagits med i projektet, den kan stå i en headerfil som du gjort #include på, eller den kan vara färdigkompilerad och stå i en objektsfil. I det sistnämnda fallet skriver man en fördeklaration till funktionen i en headerfil, för att kompilatorn ska känna till funktionens anropsegenskaper (namn, inparametrar och returtyp).

 

Objektsfiler och headerfiler kan finnas antingen i standardkatalogen eller i projektkatalogen. Finns de på annat ställe, vilket inte rekommenderas, måste man ange hela sökvägen.

 

De funktioner som följer med kompilatorn är av den sistnämnda typen. Titta i filen ...\msvc\include\stdio.h och försök orientera dig lite. Där finns bland annat typdeklarationer och fördeklarationer för de funktioner som sysslar med standard in och utmatning. Ska man få full nytta av språket C gäller det att försöka lära sig använda de funktioner som finns i de olika headerfilerna. Titta gärna i dem lite då och då.

 

Bland annat hittar du följande:

 

/* function prototypes */

.

.

.

#ifndef _WINDLL

int __cdecl printf(const char *, ...);

#endif

 


Så här deklarerar man en funktion:

 

<typ> <funktionsnamn>([<typ> <argument>][,<typ> <argument>]...)

{

    [variabeldeklaration];...

    [programsats];...

}

 

T.ex.:

 

void goddag()

{

    puts(”God dag, hur står det till?”);

}

 

Så här använder man en funktion:

 

void main()

{

    goddag();

}

 

Observera att användningen kan se olika ut beroende på om man har argument och/eller returvärde. Funktionsanrop kan också förekomma i uttryck.

 

Övningsuppgift:

·      När du startar ett C-program kommer operativsystemet att anropa en viss funktion i programmet. Vilken?

 

Övningsuppgift:

·      Nytt projekt: funkis.

·      Skriv 3 funktioner av typen void (inga parametrar). De ska bara göra var sin utskrift m.h.a. puts():
Funktionsnamn:               Text som skrivs ut:
hej()                                Hej, jag heter Rulle.
prata()                             Jag vill bara visa att det går att anropa funktioner.
hejda()                            Hej då!

·      Skriv main(), och anropa de tre funktionerna i ovanstående ordning.

 

Övningsuppgift:

·      Öppna projektet funkis, om det inte redan är öppet.

·      Ändra i funkis.c så att funktionen prata() skriver ut: Jag vill bara visa att det går att anropa funktioner flera gånger!

·      Deklarera en variabel av typen short int i main().

·      Lägg till en for-slinga så att anropet till prata upprepas 14 gånger, använd int-variabeln att räkna med.


Övningsuppgift:

·      Öppna projektet funkis, om det inte redan är öppet.

·      I funktionen prata() lägger du till ett anrop till hej() före anropet till puts().

·      Ta bort anropet till hej() i main().

 

Övningsuppgift:

·      Öppna projektet schack.

·      Gör en funktion visa() (av typen void), vilken utför själva visningen av schackbrädet d.v.s. den del av koden som förekommer två gånger i main(). Kopiera själva koden från den ena visningsdelen i main().

·      Observera att variablerna ska vara kända i både visa() och main(), så de måste flyttas ut från main() och läggas mellan #include-satsen och funktionen visa(). Huvudfunktionen main() ska alltså stå sist.

·      Ersätt de två visningsdelarna i main() med anrop till funktionen visa().

·      Testa att programmets funktion inte har ändrats.


Return: instruktion, typ och värde.

 

En funktion har ett kodavsnitt. Om funktionen inte har något returvärde kommer den att återlämna kontrollen till den anropande funktionen när den utfört alla programsatser fram till slutklammern:

 

void minfunktion()

{

   <programrader>

   .

   .

   .

}        // Här återlämnas kontrollen till den anropande     // funktionen.

 

Man kan också avbryta bearbetningen med programinstruktionen ‘return’:

 

void minfunktion()

{

   <programrader>

   .

   .

   .

   if(<nånting>)

   {

      <eventuellt programrader>

      .

      .

      .

      return;    // Här återlämnas kontrollen till den

   }             // anropande funktionen om if-satsen utförs.

   .

   .

   .

}        // Här återlämnas kontrollen till den anropande     // funktionen om if-satsen inte utförts.

 

Är funktionen av annan typ än void, så måste den lämna ett returvärde. Den sista satsen före slutklammern måste alltså vara ‘return’. För att funktionen ska lämna över ett returvärde ska detta stå efter ordet ‘return:

 

int funkis()     // Funktionen funkis är av typen int.

{

   <programrader>

   .

   .

   .

   return 0;     // Returnerar värdet noll.

}

 


Man kan använda en variabels värde att svara med.

 

int funkis2()

{

   int iSvar;

   .

   .

   .

   return iSvar; // Svarar med värdet i variabeln iSvar.

}

 

Hur tar man då emot returvärdet? Man betraktar själva funktionsanropet som ett värde av funktionens typ. Därigenom kan man t.ex. använda en tilldelnings- operator för att ta emot värdet. Man kan testa på funktionsanropet direkt med t.ex. en if-sats etc.

 

iAntal = raekna();                 // Returvärdet från funktionen                      // raekna hamnar i variabeln                        // iAntal.

 

if(raekna() == 5)         // if-satsens kodavsnitt utförs

{                         // om funktionen raekna returnerar

   ...                    // värdet 5.

}

 

if((iAntal = raekna()) == 5)

{                         // Returvärdet från raekna läggs i

   ...                    // iAntal, och om det är 5 kommer

}                         // if-satsens kodavsnitt att                         // utföras.

 

if(!strcmp(str1, str2))   // strcmp() jämför två textsträngar

{                         // och returnerar 0 om de är lika.

   ...                    // if-satsens kodavsnitt kommer att

}                         // utföras om str1 innehåller samma

                          // text som str2.

 

Övningsuppgift:

·      Nytt projekt: retur.

·      Förutom huvudfunktionen main() ska källkoden innehålla en funktion som du kallar pi(). Den ska vara av typen float, och returnera värdet på pi (3,14159).

·      Deklarera en variabel i main(). Den ska heta fPi och vara av typen float.

·      Tilldela fPi sitt värde genom att anropa pi().

·      Skriv ut: ”Pi = ” samt värdet i fPi.

·      Multiplicera fPi med 2 och skriv ut ”2 * Pi = ” samt värdet i fPi.


Anropsparametrar.

 

Skriver du funktioner i samma källkod som anropande funktion kan en eller flera variabler vara globalt kända genom att de deklareras före funktionerna.

 

Normalfallet är att en funktion endast känner sina egna variabler, de som är deklarerade i funktionen. Hur är det med de headerfiler som följer med kompilatorn?

 

En funktion kan bara ha ett enda returvärde (eller inget alls), men man kan förse funktionen med flera olika värden att arbeta med. Dessa kallas funktionens parametrar, anropsparametrar eller argument. Man räknar upp parametrarna inom de paranteser som alltid följer funktionens namn, i rätt ordning och åtskilda av kommatecken.

 

Det finns ett äldre sätt, slå upp WinMain() i hjälpen. Där står endast namnen inom parantesen, deklarationerna står efter sista parantesen och före kodavsnittets början, d.v.s. före ‘{‘.

 

När man skriver funktionen deklarerar man de variabler som fungerar som argument inom paranteserna. Det behöver inte vara samma namn som den anropande funktionen använder, men ordningen, antalet argument och typerna måste stämma överens:

 

float multi(float tal1, float tal2)

{

   float svar;                  // Deklarera intern variabel.

   svar = (float)(tal1 * tal2); // Beräkna...

   return svar;                 // ...och returnera svaret.

}                               // (Obs! returvärde är av samma

                                // typ som funktionen.)

 

Den anropande funktionen kan t.ex. göra följande:

 

float a, b;

a = multi(a, b);

 

Observera att vi passerar värden från den anropade funktionens variabler till den anropade funktionens variabler. Vi kommer att titta på ett annat sätt under avsnittet ‘Anrop med pekare’.

 

Övningsuppgift:

·      Nytt projekt: header.

·      Skriv en funktion multi() enligt ovan, och spara som header.h.

·      Skriv ett program header.c som inkluderar header.h och använder multi()

·      Programmet ska fråga efter två tal och skriva ut produkten.


Anrop med pekare.

 

När vi skickar med parametrar till en funktion kommer bara parametrarnas värden att skickas över. Funktionen har egna variabler att manipulera. Det betyder att funktionen inte kan ändra värdet på de variabler du skickar med så att din anropande funktion kan se ändringen:

 

#include <stdio.h>

void plustre(a)

int a;

{

   a = a + 3;

}

void main()

{

   int a = 4;

   printf(" Före anrop innehåller a: %d\n", a);

   plustre(a);

   printf("Efter anrop innehåller a: %d\n", a);

}

 

 

 

 

Ovanstående program ger utskriften:

 

                        

 

 

 

 

Det är t.o.m. så att a, i main(), inte är samma variabel som a, i plustre()! Vi kommer att titta mer på detta i avsnittet ‘Kända variabler’, men nu ska vi se hur man kan lösa detta genom att använda pekare.

 


Man kan i stället skicka med en pekare som parameter. Om den pekar på en variabel som den anropande funktionen känner till, kan den anropade funktionen ändra variabelns värde:

 

#include <stdio.h>

void plustre(a)

int *a;

{

   *a = *a + 3;

}

void main()

{

   int a = 4;

   int *pa;

   pa = &a;

   printf(" Före anrop innehåller a: %d\n", a);

   plustre(pa);

   printf("Efter anrop innehåller a: %d\n", a);

}

 

Nu blir utskriften:

 

                      

 

 

 

 

Observera att detta är just vad vi har gjort hela tiden med textsträngar. Det är ju nödvändigt eftersom en funktion bara kan svara med en variabel, och en text består ju av en hel lista variabler av typen char. Vad vi gjort hela tiden är att anropa med en pekare som pekar på första char-variabeln i listan. Vi har kanske bara inte tänkt på detta eftersom vi får förkorta ‘&text[0]’ till ‘text’. Egentligen ser utskrift av en text ut så här:

 

char text[] = ”Detta ska skrivas ut”;

printf(”%s”, &text[0]);

 

D.v.s. vi skickar över textvariabelns adress till printf. Om man förkortar &text[0] som vanligt ser det mer vant ut:

 

char text[] = ”Detta ska skrivas ut”;

printf(”%s”, text);

 


Dessutom måste vi inte ha en strängvariabel, det går att ‘hårdkoda’ text i programmet. Då kommer texten att lagras tillsammans med programkoden, och vid den instruktion där texten används lagras adressen till texten. Därför kan man skriva:

 

printf(”Detta ska skrivas ut”);

 

 

Övningsuppgift:

·      Projekt myhead. Här kan vi lägga till egna nyttofunktioner i en fil myhead.h.

·      Vi börjar med att skapa en funktion för att byta två tal av typ int med varandra, så slipper vi göra byte via temporär variabel i fortsättningen.

·      Funktionen ska heta ibyt() och ta emot två pekare till typen int. Inget returvärde.

·      Skriv en källkod myhead.c där du gör ett program för test av funktionen.

 

Övningsuppgift:

·      Öppna projektet myhead om det inte redan är öppet.

·      Lägg till funktioner för byte av två variabler, vilka hanterar typerna short, long, float, double samt char. De ska heta sbyt(), lbyt(), fbyt(), dbyt() resp. cbyt().

·      Ändra i myhead.c så att du kan testa de nya funktionerna.

 

Övningsuppgift:

·      Lägg till funktionerna ucase() och lcase() i myhead.h.

·      Dessa ska ta emot en pekare till en c-sträng (en lista av char avslutad med NULL-tecken).

·      De ska byta alla bokstäver till stora (ucase, upper case) respektive små (lower case) bokstäver. De tecken som inte är bokstäver lämnas orörda. De bokstäver som har rätt storlek ska inte heller ändras.

·      De ska inte returnera något svar.

·      Ändra i myhead.c så att du kan testa ucase() och lcase().

 


Prototyper.

 

Innan man anropar en funktion måste kompilatorn känna till den. Detta gör att man vanligen deklarerar funktionerna i sådan ordning att de står ovanför de funktioner som anropar dem:

 

int kvadrat(a)

int a;

{

    a = a * a;

    return a;

}

 

void main()

{

    int a, b;

    a = 5;

    b = kvadrat(a);

}

 

Det är dock inte alltid möjligt att göra så här. Dels kanske funktionen redan är kompilerad, och inte tillgänglig som källkod, dels kanske vi har något som ser ut ungefär så här:

 

int funka(a)

int a;

{

    int b;

    ...

    a = funkb(b);

    ...

    return a;

}

 

int funkb(*b)

int *b;

{

    int a = 3;

    if (*b > 5)

    {

        *b = funka(a);

    }

}

 

Vilken ordning vi än skriver dessa funktioner i kommer den ena att vara okänd för kompilatorn när den andra läses först. Detta dilemma löses m.h.a. en s.k. prototyp.

En prototyp är helt enkelt en funktionsheader utan tillhörande funktion. Därav namnet headerfiler och filnamnstillägget .h. I dessa filer finns prototyper som beskriver egenskaper hos de färdigkompilerade funktioner som följer med C++.

Ovanstående dilemma kan lösas på följande sätt:

 

void funkb(int*);    // Prototyp för funktionen funkb().

 

int funka(a)

int a;

{

    int b;

    ...

    a = funkb(b); // Kompilatorn känner till funkb().

    ...

    return a;

}

 

void funkb(int *b)     // Här kommer funkb() i sin helhet.

{

    int a = 3;

    if (*b > 5)

    {

        *b = funka(a);

    }

    else

    {

        *b = 0;

    }

}

 

Observera semikolon efter prototypen. Det står där bara för att tala om att vi inte kommer att skriva mer om funktionen just nu. I normalfallet följer ju ett kodavsnitt, och kompilatorn anser att vi är klara när kodavsnittet avslutas med en slutklammer.

 

När man nu kan göra på detta sättet är det många som föredrar att lägga main() i början av programmet, endast föregånget av prototyper och diverse kompiler­ings­direktiv.

 

 

Övningsuppgift:

·      Program proto. Innehåller förutom main() en funktion kvadrat() vilken är av typen float och returnerar kvadraten på den parameter (float) som anges.

·      Funktionen kvadrat() ska stå sist i källkoden.

·      Använd en prototyp för att göra kvadrat() känd när main() kompileras.

·      Main frågar efter ett tal och matar in det.

·      Därefter anropas kvadrat() och resultatet skrivs ut med lämplig ledtext.

·      Alltihop upprepas tills användaren anger en bokstav i stället för ett tal.


Inlinefunktioner.

 

Inlinefunktioner har vissa likheter med macros (nästa avsnitt). Skillnaden är att man behåller samma syntax som vid vanliga funktionsanrop.

 

Låt oss jämföra inlinefunktioner med vanliga funktioner. När man skriver en vanlig funktion läggs den in i ett exemplar på ett ställe i programmet. När den sedan anropas sker följande:

 

1.   Adressen till nästa instruktion sparas på stacken.

2.   Varje parameter sparas.

3.   Hopp sker till den adress där funktionen ligger.

4.   Parametrarna hämtas från stacken till funktionens lokala variabler.

5.   Funktionens programkod körs.

6.   Returadressen hämtas från stacken.

7.   Återhopp sker.

 

Skriver man en inlinefunktion läggs den in på varje ställe i programmet där den anropas. Därigenom sparar man in steg 1, 3, 6 och 7 enligt ovan.

 

För och nackdelar?

 

En uppenbar nackdel är att man använder mer minne. På varje ställe där man använder funktionen läggs koden in i sin helhet. Det förekommer också ett par restriktioner, se nedan.

 

Den egentliga fördelen ligger i att man sparar in den tid det tar att lagra nästa instruktions adress på stacken, göra hoppet till funktionen, hämta adres­sen igen och hoppa tillbaka. Det är inte mycket tid man sparar, så fördelen ligger i funktioner som är korta (relativa tidsskillnaden märkbar) och anropas ett stort antal gånger (loopar).

 

Det är alltså inte ofta man behöver inlinefunktioner. Man kan spara ännu mer tid genom att inte använda någon funktion. Då slipper man passera parametrar till den, och man slipper returnera ett värde. Man jobbar direkt med de variabler man har. Då måste man dock skriva in koden på varje ställe den behövs. Observera att ofta förekommen kod kan ersättas m.h.a. #define, se nedan.

 

Man skriver en inlinefunktion genom att lägga nyckelordet 'inline' först i funktionens header.


Restriktioner:

 

Inlinefunktionen måste beskrivas i sin helhet innan den kan anropas,

d.v.s. det räcker inte med bara headern, s.k. prototyp som beskrevs i föregående avsnitt. Detta kan man kanske tycka vara en nackdel.

 

En annan nackdel är att man inte får använda rekursion med en inlinefunktion, d.v.s. den får inte lov att anropa sig själv.

 

Som exempel kan vi göra en liten kvadratfunktion:

 

inline double kvadrat(double x)

{

    return x*x;

}

 

Förutom syntaxen kan det vara värt att observera att funktionen är mycket kort. Längre funktioner har inte någon större relativ tidsvinst.

 

Övningsuppgift:

·      Skriv ett program inline som tar emot ett tal i taget.

·      Om talet är större än noll ska ovanstående inlinefunktion anropas och resultatet ska skrivas ut enligt följande:
                 "Kvadraten på <tal> är <kvadraten>."
varefter nytt tal efterfrågas.

·      Om talet är noll ska programmet avslutas.

·      Det ska se ut t.ex. så här:

 

 

                


Macros.

 

Kompileringsdirektivet #define kan användas för att skapa enkla macros. Det går alltså även att använda anropsvariabler, d.v.s. en motsvarighet till funk­tioner­nas anopsparametrar. Detta är egentligen ingen äkta användning av parametrar utan bara textsubstitution, men kan vara väl så användbart.

 

Testa nedanstående:

 

#include <iostream.h>

#define KUB(X) ((X)*(X)*(X))

 

void main(void)

{

    float fSida = 1, fVolym;

    cout << "Jag beräknar kuber åt dig.\n\n";

    while(fSida > 0)

    {

        cout << "Ange kubens sida: ";

        cin >> fSida;

        if(fSida > 0)

        {

            fVolym = KUB(fSida);

            cout << "Kubens volym = " << fVolym << ".\n\n";

        }

    }

}

 

Så här kan ett körexempel se ut:

 

                

 

 

Övningsuppgift:

·      Kalla programmet macro.

·      Lös föregående uppgift (kvadrat) m.h.a. ett macro i stället.

 


Låt oss göra en jämförelse mellan inline-funktioner och makro. Increment operator (ökningsoperatorn, '++') ökar värdet på en variabel före respektive efter att den har använts. Det kan t.ex. se ut så här i funktionen strcpy() (som används för att kopiera en sträng till en annan, '\0' avbryter kopieringen):

 

void strcpy(char *to, char *from)

{

    while(*to++ = *from++);

}

 

Själva while-satsen översätts till nedanstående maskinkod:

 

0D8F:005D 8B5E08         MOV       BX,WORD PTR [from]

0D8F:0060 83460801       ADD       WORD PTR [from],01 

0D8F:0064 8A07           MOV       AL,BYTE PTR [BX] 

0D8F:0066 8B5E06         MOV       BX,WORD PTR [to] 

0D8F:0069 83460601       ADD       WORD PTR [to],01 

0D8F:006D 8807           MOV       BYTE PTR [BX],AL 

0D8F:006F 98             CBW        

0D8F:0070 3D0000         CMP       AX,0000 

0D8F:0073 7503           JNZ       strcpy+0028 (0078) 

0D8F:0075 E90300         JMP       strcpy+002B (007B) 

0D8F:0078 E9E2FF         JMP       strcpy+000D (005D)

 

Här ser man hur de två pekarna ökas ett steg efter att de används: först används 'from', sedan ökas 'from' med 1, sedan används 'to' och till sist ökas 'to' med 1.

 

Använder man makro kan det uppstå ett problem vilket belyses av nedanstående exempel, där makrot 'MAX()' jämförs med inlinefunktionen 'max()':

 

#include <iostream.h>

#define MAX(A, B) ((A)>(B)?(A):(B))

 

inline int max(int a, int b)

{

    return a > b ? a : b;

}

 

void main()

{

    int i, x, y;

 

    x = 23;

    y = 45;

    i = MAX(x++,y++); // Observera att y ökas två ggr!

    cout "i = " << i << " x = " << x

         << " y = " << y << '\n';

 

    x = 23;

    y = 45;

    i = max(x++,y++); // Fungerar som förväntat.

    cout "i = " << i << " x = " << x

         << " y = " << y << '\n';

}

 

Utskriften visar bieffekten, på första raden har y ökats till 47 i stället för 46, d.v.s. två gånger:

 

i = 46 x = 24 y = 47

i = 45 x = 24 y = 46

 

Som övningsuppgift bör eleven själv testa ovanstående utan att skriva av uppgiften. Han bör själv kunna konstruera koden ungefär enligt ovan, och demonstrera den dubbla ökningen.

 

Betraktar man maskinkoden ser man tydligt den extra användningen. Du bör därför starta programmet i läge 'debug', trycka på Alt + 8 och själv studera maskinkoden.


Rekursion.

 

En rekursiv funktion är en funktion som anropar sig själv. När den gör detta kommer den i det nya anropet att anropa sig själv igen. Detta fortsätter tills stacken blir full, sedan krashar programmet.

 

För att kunna göra något vettigt av en rekursiv funktion måste man hålla reda på antalet anrop, och låta bli att anropa när man kommit till ett visst läge.

 

Testa detta exempel:

 

#include <iostream.h>

 

void rekurs(long);

void main(void)

{                              

    long tal = 0;              

    cout << "Ange ett litet heltal: ";

    cin >> tal;

    if (tal > 0)

    {

        rekurs(tal);

    }   

}                      

void rekurs(long tal)

{                  

         int anrop;

 

         anrop = tal;

         cout << "Detta är anrop: " << anrop << "\n";

         tal = tal - 1;

         if (tal > 0)

         {

                 rekurs(tal);

         }                                         

         cout << "Vi är klara med anrop: " << anrop << "\n";

}

 

Så här kan det se ut när man testar programmet och anger en trea:

 

                


Övningsuppgift:

·      Gör ett program som beräknar fakulteten av ett tal.

·      Kalla programmet fakultet.

·      Fakulteten av ett Tal n betecknas tal! (med utropstecken).

·      Tal! = Tal * (Tal - 1) * (Tal - 2) * ... * (Tal - n) där n = Tal - 1.

·      Specialfall: 0! = 1.

·      Använd en rekursiv funktion.

·      Exempel på utskrift:

 

                

 

                


Förvalda argument.

 

Mera ketchup på glassen! Ytterligare en nyhet i C++ är att man kan ange förval­da argument, vilka används i de fall där man inte anger argumenten vid anrop.

 

Man inser snabbt att detta ger ytterligare en möjlighet att förvirra den stackare som senare ska göra ändringar/tillägg/rättningar i ditt program. Rätt använt kan det dock ha sitt värde.

 

Låt oss skapa en motsvarighet till Basics funktion LEFT$. För att rätt utnyttja förvalda argument tillåter vi att man utesluter den parameter som anger hur många tecken som ska tas med. Gör man det ska funktionen svara med en textsträng med bara ett tecken.

 

Så här kan det se ut:

 

#include <iostream.h>

 

char *left(char *, int i = 1);

 

void main(void)

{                              

    char cText[81];

    int iAntal;

    char *cLeft;

        

    cout << "Skriv en text!\n";

    cin.getline(cText,80); 

    cout << "Ange hur många tecken jag ska visa: ";

    cin >> iAntal;

    cLeft = left(cText, iAntal);

    cout << "De första " << iAntal << " tecknen i texten är: "

         << cLeft << "\n";

    cLeft = left(cText);

    cout << "Den första bokstaven i texten är: "

         << cLeft << "\n";

}                 

 

char * left(char *pIn, int i)

{                          

    int j = 0;

    char *pUt = new char[i+1];

    while(j < i)

    {

        pUt[j] = pIn[j];

        j++;

    }        

    pUt[j] = '\0';

    return pUt;

}
Observera hur man gör ett argument (en parameter) förvald. Det första kom­pi­la­torn ser av en funktion är dess prototyp. (Se exemplet ovan.) Skriv in argu­mentet med namn och värde, så betraktas det som förvalt. När sedan funktionen beskrivs behöver man inte göra något speciellt.

 

Ovanstående exempel kan se ut så här när man kör det:

 

 

 

        

 

 

 

Övningsexempel:

·      Tillverka right() p.s.s. som ovan.

·      Tillverka en main() som du kan testa right() från.

 

 

 

Övningsexempel:

·      Tillverka mid() p.s.s. som ovan.

·      Startposition måste anges.

·      Antal tecken förväljs till 1 om tredje parameter inte anges.

·      Tillverka en main() som du kan testa mid() från.


Övningsexempel:

·      Gör en funktion datum() som lagrar datum i nedanstående variabler:

                 unsigned aar;

                 unsigned short maanad;

                 unsigned short dag;

·      Funktionen ska ta emot variablerna i nämnd ordning, och alla ska vara förvalda, så att man kan utesluta dag, dag och månad, eller alla tre argumenten.

·      Funktionen ska, om argument saknas, sätta förvalda till: aar = 1990, maanad = 1, dag = 1.

·      Funktionen ska innehålla kontroller av angivet data. Om ett värde är för stort sätts det till största tillåtna värde, om det är för litet sätts det till lägsta tillåtna värde. Om år anges utan århundrade (< 100) ska värdet ökas med 1900. Lägsta värde för år är 1900, högsta är 2100.

·      Varje gång funktionen har lagrat ett datum ska det visas upp på skärmen.

·      Skriv en main() som låter användaren mata in datum upprepade gånger. Användaren ska uppmanas att ange antal argument som ska anges, och beroende av detta svar (0 - 3) ska argumenten efterfrågas, varefter funktionen anropas med rätt antal argument.

 

 


Överlagrade funktioner.

 

Polymorfism är utrikiska och betyder 'flerformig'. När det gäller C++ program­mering talar man också om 'överlagrade funktioner' (overloaded functions).

 

Vaddå 'flerformig', vaddå 'överlagrade'? Som vanligt gäller det att skapa nya termer, vilka på effektivast möjliga sätt håller novisen på mattan. (It's done only to confuse the Russians.)

 

Vi som lärt oss litet om C-programmering inser dock snabbt att man äntligen löst problemet att kunna använda funktioner med flera olika former trots att de har samma namn.

 

Kommer ni ihåg övningsexemplen från avsnittet ’Anrop med pekare’, de som byter värden mellan två variabler av samma typ? Vi var tvugna att skriva en ibyt(), en cbyt(), en fbyt etc. Vad vi nu kan göra, i och med C++, är att ge dem alla samma namn. Kompilatorn skiljer dem åt genom att kontrollera argumentens typ.

 

Om man deklarerar två funktioner med samma namn skulle C-kompilatorn genast protestera. C++ godkänner dem dock under förutsättning att den kan se någon skillnad i argumentlistan. Så länge den inte har samma antal argument med samma typer i samma ordning, kommer den att betrakta den andra som en ny, överlagrad funktion.

 

Deklarera de olika överlagrade funktionerna genom att skriva funktions­proto­typer med de olika argumentlistorna:

 

void byt(int *a, int *b);

void byt(float *a, float *b);

etc.

 

 

 

Övningsuppgift:

·      Ändra i ovannämnda uppgift från avsnittet ’Anrop med pekare’, men använd polymorfism.

·      Funktionerna ska alla heta byt().

·      De ska klara av typerna int, float, double, char, samt long.

·      Skriv en main() som klarar av att testa alla varianterna av byt.

 


Ibland använder man överlagrade funktioner för att anpassa användandet av data som lagrats på olika sätt trots att det har i grunden samma betydelse.

 

T.ex. tid kan finnas lagrat i en stuct tm, men även i en time_t. Den förra är en struktur med heltalsvariabler för tid, datum och årtal, medan den senare är ett långt heltal avsett att innehålla samma information i packat format. Bägge finns i filen time.h. Öppna den och studera de bägge formaten, samt se vilka funktio­ner som använder dem!

 

De funktioner som finns tillgängliga är inte de samma för de två dataformaten, naturligtvis. Vi kan dock nu skaffa oss funktioner som använder valfritt format via överlagrade funktioner. Låt oss anta att vi vill skriva ut ett klockslag och vill använda de färdiga formatfunktioner som finns. De två olika formaten kräver att vi använder olika funktioner. Då bakar vi bara in dem i två överlagrande funktioner, vilka samtidigt utför själva utskriften:

 

#include <iostream.h>

#include <time.h>

 

void display_time(const struct tm *tim)

{

    cout << asctime(tim);

}

 

void display_time(const time_t *tim)

{

    cout << ctime(tim);

}

 

void main()

{

    time_t tim = time(NULL);

    struct tm *ltim = localtime(&tim);

   

    display_time(ltim);

    display_time(&tim);

    cout << ‘\n’;

}

 

Funktionerna asctime() och ctime() returnerar adressen till en textsträng vilken formaterats för utskrift av datum och klockslag, men de två funktionerna kräver olika format på sitt argument. Slå upp dem i hjälpen och läs. Förklara varför jag inte har lagt till något nyradstecken i utskriftssatserna.

 

Slå även upp funktionerna time() och localtime() för att förstå den första halvan av main().

 


Så här blir utskriften:

 

 

Och därmed vet ni när jag skrev detta kapitel.

 

Observera att funktioner överlagras genom att man anger samma namn på funktionen, och gör skillnader i argumentlistan. Observera att detta går att kombinera med förvalda argument, trots att detta tillåter skillnader i just argumentlistan (olika antal argument). Kompilatorn kontrollerar så att man inte får någon konflikt:

 

void funk_a(int x, int y = 0); // Prototyp, ger förvalt y = 0.

...

// Funktionsdeklarationer:

void funk_a(int x, int y)

{

    ...

}

void funk_a(int x) // Fel! Samma som ovan om y inte anges.

{

    ...

}

 

Kompilatorns besservisser-kommentar:

 

'error C2668: 'funk_a' : ambiguous call to overloaded function'

 

Vilket INTE antyder att vi har kollision med förvalt värde, bara att det inte går att skilja funktionerna. Svårt att se i källkoden om funktionsprototypen inte syns samtidigt med funktionsdeklarationerna på skärmen.

 

Överlagrade funktioner får ha olika returvärden, vara av olika typ, men det är inte typen som avgör att funktionen överlagras. Skillnaden måste finnas i argumentlistan. Det räcker med olika antal argument, eller med olika typ på minst ett argument. Om endast returtypen skiljer får man ett felmeddelande:

 

'error C2556: <funktion> : overloaded functions only differ by return type'

 


Övningsuppgift:

·      Slå upp funktionerna strcpy() och strncpy() i hjälpen. Se vad som skiljer dem. Vilken #include behövs?

·      Skapa två funktioner vilka bägge heter string_copy(), och som överlagrar varandra. Den ena ska anropa strcpy() och den andra strncpy(), varvid de skickar vidare samma argument som de själva fått. Därigenom får vi ett namn att anropa, oavsett om vi vill kopiera hela strängen eller en del av den.

·      Skriv en main() som provar de överlagrade funktionerna enligt nedanstående:

 

void main()

{

    char cText1[25], cText2[25];

 

    string_copy(cText1, "Grannens katt och vår katt ", 14);

    string_copy(cText2, "jamar på natten.");

    cout << cText1 << cText2 << '\n';

}

 

Utskriften ska bli:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Räckvidd.

 

Som vi redan sett kan man deklarera variabler på olika ställen. Det som är intressant är att de därigenom har olika räckvidd. Skriver man en källkod med flera funktioner, alternativt lägger funktioner i headerfiler, kan man deklarera en variabel i början av källkoden, eller i en funktion.

 

En variabel som deklarerats i början av källkoden (utanför första funktionen) är känd av alla funktioner i källkoden. Den kallar vi ‘globalt deklarerad’.

 

En variabel som deklarerats inuti en funktion är känd bara i den funktionen. Den kallar vi ‘lokalt deklarerad’. Dess räckvidd (scope) är begränsad.

 

Om dom har samma namn då?

 

Två variabler som deklarerats lokalt med samma namn i två olika funktioner är i praktiken två olika variabler, d.v.s. de lagras på två olika ställen i minnet.

 

En variabel som deklarerats globalt är känd i alla funktioner, men om någon funktion har en lokalt deklarerad variabel med samma namn är den globalt deklarerade variabeln inte längre känd.

 

#include <stdio.h>

int a;           // Känd av alla funktioner.

int b;           // Känd av main()och funk_b()

                 // men ej av funk_a().

 

void funk_a()

{

   int b;                 // Lokal variabel, hindrar referens

   a = 1;                 // till globala b.

   b = 2;

}

 

void funk_b()

{

   a = 3;                 // Endast globala variabler.

   b = 4;

}

void main()

{

   a = 5;

   b = 6;

   printf("Variabeln a = %d och b = %d\n", a, b);

   funk_b();

   printf("Variabeln a = %d och b = %d\n", a, b);

   funk_a();

   printf("Variabeln a = %d och b = %d\n", a, b);

}
Ovanstående program resulterar i följande:

 

 

 

 

 

Övningsuppgift:

·      Skrivbordstesta ovanstående exempel.

·      Skriv allt snyggt på papper: när en variabel byter värde stryker du över det gamla värdet och skriver det nya bredvid (så att men kan se hur du tänkt).

·      Redovisa för din handledare.

 

Övningsuppgift:

·      Skapa projektet scope.mak.

·      Skriv av exemplet ovan med följande skillnad:

·      Flytta funk_a till en headerfil scope.h och gör ‘#include’ på den.

·      Kompilera.

·      Förklara varför kompilatorn klagar på scope.h.

·      Lägg till det som saknas i scope.h.

·      Kompilera och testa.

·      Förklara varför utskriften inte ändras mellan rad 2 och 3.

·      Redovisa för din handledare.

 

Övningsuppgift:

·      Öppna projektet scope.mak om det inte redan är öppet.

·      Öppna filen scope.h och lägg till ordet ‘extern’ före ordet ‘int’ där du deklarerar a och b.

·      Kompilera och testa.

·      Nu fungerar det, varför ska vi se i nästa avsnitt (lagringsklasser).


Räckviddsoperatorn.

 

Räckviddsoperatorn, Scope Resolution Operator.

 

I C infördes begreppet räckvidd (scope) i två nivåer. En variabel kunde vara global, och därigenom känd i alla sammanhang som kompileringen omfattar, s.k. file scope. Observera att även filer som inkluderats med #include ingår i filsammanhanget (file scope). Viktigt är att en global variabel inte är känd i förväg. Man kan alltså ha ett partiellt file scope genom att inte deklarera variabeln i början.

 

Om man sedan deklarerade en variabel i en funktion, fick den en räckvidd inom funktionens kodavsnitt. Detta kallades för en lokal variabel.

 

I C fanns alltså globala och lokala variabler. Om man deklarerade en lokal variabel med samma namn som en global variabel, förlorade den lokala variabelns sammanhang möjligheten att nå den globala variabeln:

 

#include <stdio.h>

int iVar; // Global variabel.

 

void funk_a()

{

    int iVar; // Lokal variabel.

 

    iVar = 5;

    printf("I funk_a(): iVar = %d\n", iVar);

}

 

void main()

{

    iVar = 3;

  

    printf("Före anrop: iVar = %d\n", iVar);

    funk_a();

    printf("Efter anrop: iVar = %d\n", iVar);

}

 

Detta skulle resultera i följande utskrift:

 

Före anrop: iVar = 3

I funk_a(): iVar = 5

Efter anrop: iVar = 3

 


Vill man i funk_a() använda den globala iVar i stället, är det bara att låta bli att deklarera den i funk_a(). Om man av någon anledning ändå vill deklarera den, kan man använda nyckelordet 'extern' för att tala om att man avser en global variabel i stället:

 

void funk_a()

{

    extern int iVar;

...

 

I C++ kan man både äta kakan och ha den kvar, tack vara räckviddsoperatorn. Den skrivs med två kolon (::) före variabelnamnet för att ange att man avser den globala variabeln. Om vi nu ändrar exemplet från ovan:

 

#include <iostream.h>

int iVar; // Global variabel.

 

void funk_a()

{

    int iVar;   // Lokal variabel.

 

    iVar = 5;   // Lokala iVar sätt till 5.

    ::iVar = 10 // Globala iVar sätts till 10.

    cout << "I funk_a(): iVar = " << iVar;

    cout << ", ::iVar = " << ::iVar << '\n';

}

 

void main()

{

    iVar = 3;

  

    cout << "Före anrop: iVar = " << iVar;

    funk_a();

    cout << "Efter anrop: iVar = " << iVar);

}

 

Detta skulle resultera i följande utskrift:

 

Före anrop: iVar = 3

I funk_a(): iVar = 5, ::iVar = 10

Efter anrop: iVar = 10

 

Observera följande:

·     Vi kunde använda den globala variabeln i funk_a().

·     Vi kunde påverka den globala variabeln i funk_a(), så att den sista utskriften från main() ändrades.

·     Den i funk_a() lokala variabeln går inte att nå ifrån main().

 

I C måste man dela in ett kodavsnitt så, att man först skriver alla deklarationer, och sedan all kod. Om man skriver en deklaration efter en programsats kommer kompilatorn att klaga.

 

C++ tillåter däremot valfritt antal sammanhang. Man kan deklarera variabler mitt i koden, och beroende av pågående sammanhang får variabeln begränsat sammanhang. Man kan t.ex. deklarera en variabel i ett kodavsnitt till en villkorlig bearbetning som if, while, switch etc.

 

...

if(<villkorl>)

{ // Lokalt 'mikro-sammanhang' börjar.

    for(int i = 0; i < 10; i++) // i deklareras!

    {

        ...

    }

    for(;i > 0; i--) // i lever fortfarande!

    {

        ...

    }

else

{

    while(i > 0) // Fel! Detta är ett nytt sammanhang!

    {

        ...

    }

}